Software Architecture

Clean Code in PHP

Not just tidy, but clean

Carsten Windler

Do you want to create software that is easy to maintain and has as few bugs as possible? Or write code that others, but also you, can still understand after weeks or even months? Maybe clean code is something for you.

Most software developers, of course, want to write “good” or “clean” code. But it’s not that trivial; otherwise, we wouldn’t be discussing it. But where do we begin? And what, after all, is “clean” code?

What exactly is “clean code”?

The term “clean code” was popularised by Robert C. Martin, also known as Uncle Bob, and his standard work of the same name. [1]. There is no truly uniform definition; in essence, it is code that is written simply and directly and can be read, understood, and modified without difficulty by other developers. Clean code is free of duplication, does not obscure the intentions of the authors, and is thoroughly covered by automated testing.

Over the years, many proven solutions and principles in the field of software development have emerged, on which clean code is based. These are not limited to a single programming language; many of the principles and approaches discussed later in this article also apply to most object-oriented languages.

To be clear, writing clean code is a time-consuming process. Unfortunately, we cannot teach you all the necessary skills in this article. Rather, we want to provide you with the resources you need to get started on this path, as well as pique your interest.

IPC NEWSLETTER

All news about PHP and web development

 

Naming

It is not always easy to name things correctly, that is, in a meaningful way. That is why naming is so important in clean code.

Although it should be common knowledge that variables are no longer abbreviated with a few letters, this practise persists. Count variables like $i and $j may still be borderline cases for simple loops, but using $row and $column instead, for example, to iterate over a CSV file, will help.

Names should be concise but meaningful. Placeholders like Manager or Processor, on the other hand, add no value, and humorous names or puns should be avoided. Cryptic and unpronounceable abbreviations cause more confusion than they solve. However, it is helpful to define fixed words for specific concepts. So, instead of using a combination of set, update, and put for updating values, stick to one word.

So don’t be upset if you spend some time deciding on names for variables, classes, or methods. Take your time; it will be well worth it. Also, be open to change and listen to feedback from other developers.

Design principles

Clean code is based on several design principles that we should keep in mind when writing code. We will present a few basic principles in the following part, though you have probably encountered one or two of them elsewhere.

DRY

DRY (Don’t Repeat Yourself) is possibly the oldest programming principle. Even in the early days of software development, it felt necessary to outsource repetitive code into subroutines, primarily to save precious memory. As programmes grew, it became clear that redundant code should be avoided to ensure maintainability.

However, the opposite of DRY is all too common: copy-and-paste programming. A piece of code is quickly duplicated, and evil takes its course. Bugfixes or other changes must be made in multiple locations if they can be found at all. Each copy is then altered slightly to perfect the chaos. Resist the temptation to allow code duplication. It will undoubtedly come back to haunt you one day.

KISS

KISS (Keep It Simple, Stupid) reminds us to look for the simplest solution to a problem. I wonder who hasn’t felt this way: we’re proud of our abilities and want to flaunt them. Then, at the next available opportunity, we overshoot and write a fantastic algorithm for which there is already a thoroughly tested function in the SPL library (Standard PHP Library) that comes with PHP by default. A quick internet search on the issue in question would have saved us a lot of time. Clean code is as simple as possible. Although adventurous one-liners demonstrate how well we can juggle ternary operators, they are much more difficult to read and maintain than a well-readable if statement over several lines, as shown in Listing 1.

// nested ternary operators
$number > 0 ? 'Positive' : ($number < 0 ? 'Negative' : 'Zero');
 
// If statement
if ($number > 0) {
    echo 'Positive';
} elseif ($number < 0) {
    echo 'Negative';
} else {
    echo 'Zero';
}

It doesn’t help anyone if the bug is not found quickly the next time it fails on the production system because it has been successfully hidden in our code art. In any case, our colleagues will not thank us for it.

YAGNI

YAGNI (You Ain’t Gonna Need It) is a continuation of KISS: don’t programme anything you don’t need right now. The code remains leaner, making it easier to maintain.

Because there are more requirements than you expected, the probability is that you will have to discard the code or radically change it. In the worst-case scenario, the unnecessary code remains in your software. After a while, nobody understands why it was written in the first place. And reading. understanding, testing, and making changes to the source code costs time.

This is not to say that we should not plan ahead of time. If it is clear that a feature we are currently working on will be made multilingually translatable in the next sprint, it can’t hurt to keep this in mind and take sensible precautions.

YAGNI, like KISS, is meant to encourage us to simplify by always questioning our decisions. Is a composer package really required for a single function? Is it really necessary to programme a process, or are there ready-made solutions on the market?

YOU LOVE PHP?

Explore the PHP Core Track

 

SOLID

Of course, SOLID cannot be left out of this list. SOLID is an acronym for a set of five principles that are commonly used in object-oriented programming and are thus ideal for PHP. Let’s look at each principle in more detail below.

Single Responsibility Principle (SRP): The SRP states that a module (typically a class) should only change for one reason. This does not, however, imply that each class should have only one responsibility, as is commonly assumed [2]. Rather, it means that the module should be accountable to only one actor.

For example, if a module contains two functions, one of which is used by actor 1 and the other by actor 2, these two functions should not be in the same class. If changes are later required for one actor, the behaviour of the other function may be affected (and thus indirectly affect the other actor).

Open Closed Principle (OCP): The behaviour of a class should be extensible without requiring modification. We can accomplish this primarily through polymorphism, which is the use of a single interface for multiple manifestations of an object.

To be prepared for different use cases, we define an interface for the method rather than inflating the code with multiple if-else constructs. This interface is then implemented by different classes, which implement this method in a variety of ways, depending on the use case.

A common example is a function called readFile() that is supposed to read data from both CSV and Excel files. Rather than combining these two tasks in a single class, we define an interface for readFile(), which is then implemented by two classes: one for CSV files and one for Excel files.

Liskov Substitution Principle (LSP): Behind the complicated name lies the simple principle that subtypes must behave like their base type. They may only extend, but not change, the functionality of the base type.

These may be obvious properties like Return Types, which an inherited class should not change. When we use Return Types, we force the inherited class to behave like the base class, at least in terms of return values, as shown in Listing 2.

class BaseClass
{
    public function getValue()
    {
        return 1;
    }
}
 
// violation of LSP because return  // type changes
class SubClass extends BaseClass
{
    public function getValue()
    {
        return 'result: ' . $this->getValue();
  }
}

Without Return Types, the above example would be valid PHP code. If Return Types were used, the LSP violation would result in an error, as shown in Listing 3.

class BaseClass
{
    public function getValue(): int
    {
        return 1;
    }
}
 
// Causes PHP error, because  // return type does not match the // of BaseClass
class SubClass extends BaseClass
{
   public function getValue(): string
    {
        return 'result: ' . $this->getValue();
    }
}

However, less obvious behaviour, such as throwing exceptions in certain circumstances, should be consistent with the base type. If the behaviour here is inconsistent, bugs are pre-programmed, which are frequently difficult to find.

Interface Segregation Principle (ISP): The Interface Segregation Principle states that a client should only rely on the specifics of a service that it requires. For example, a class that implements an interface should only implement the methods that are truly required for the use case.

Instead of providing a comprehensive interface that is ready for any eventuality and contains a correspondingly large number of methods, it is preferable to implement several, specialised interfaces that focus on a single aspect. The advantage of this is that the class has fewer dependencies, reducing so-called coupling. Since PHP allows classes to implement multiple interfaces, this principle is simple to put into practise.

Dependency Inversion Principle (DIP): To create a system that is easy to maintain and resilient, we must consider which code parts may be particularly affected by changes. The file system is an example: rather than always writing to a local drive, we should extract that part and move it to a low-level class. For example, if we want to store our files in a cloud bucket because our application is no longer running on a single server but in the cloud (which happens quite often), we simply swap out the low-level class without modifying the high-level class.

We implement an interface for file system accesses and thus reverse the dependency so that the high-level class is not required to be aware of the implementation details of the dependency. The high-level class is only aware of one interface, and we decide which low-level class to pass at runtime. Important: Dependency always flows from high to low; a low-level class never depends on a high-level class.

Consider the following example. The store() method of a ReportService is intended to store the contents of a report on a storage device, in this case an S3-compatible cloud storage (Listing 4).

class ReportService
{
    public function store(
        string $filename,
        string $content
    ): void {
        Filesystem::storage('s3')->put($filename, $content);
    }
}

In the preceding example, a fictitious framework provides an easy-to-use Filesystem helper class that allows access to the configured storages at any time and from any location in the code via static function calls.

This is very convenient, but it has significant drawbacks. Because the dependency on the Filesystem helper is hardcoded into the ReportService class, switching to another medium is not easy and requires changes to the class. Furthermore, the static calls make testing difficult. The approach shown in Listing 5 would be better in terms of DIP.

interface StorageInterface
{
    public function put(
        string $filename,
        string $content
    ): void;
}
 
class ReportService
{
    public function __construct(
        private StorageInterface $storage
    ) {
    }
 
    public function store(
        string $filename,
        string $content
    ): void {
        $this->storage->put($filename, $content);
    }
}

First, the dependency is passed directly to the class in the constructor. As a result, we could easily replace the Storage class when instantiating it. We ensure that the implementation remains compatible by using the StorageInterface. This approach, of course, requires a little more effort, especially since the so-called binding, i.e., the binding of the StorageInterface to the class to be injected, is still missing in this example. However, you will be rewarded with the greatest amount of flexibility possible.

You are correct if you are thinking of Dependency Injection (DI): DI is the implementation of the Dependency Inversion Principle in all common PHP frameworks.

IPC NEWSLETTER

All news about PHP and web development

 

Other principles

We cannot present all relevant principles in detail in this article due to space constraints, so here are a few more in quick succession:

Scout Rule: The Scout Rule reminds us to leave our campsite (i.e., our codebase) in better condition than we found it. So, if you’re already working on a class, try cleaning it up a little by deleting obsolete comments or splitting an overly long method into several short ones.

Favour Composition Over Inheritance: By composing classes instead of inheritance we promote loose coupling of a system, after all inheritance keeps the subclass dependent on the base class.

Information Hiding Principle: Only the most important details should be visible to the outside world through an interface. This also applies to implicit interfaces, such as the public details of a class. So only make public the methods or characteristics that are absolutely necessary.

Principle of Least Astonishment: Software should not surprise users. The getLastName() method of a user object should return only its last name. It should not change the state of the system (e.g., through write database accesses).

Design patterns

Admittedly, that was already a lot of principles (and by no means all of them). However, these principles are always abstract; if we want clearer instructions for action, we must look more closely at design patterns.

Design patterns are commonly used solutions for specific use cases. Of course, these best practises are not universal solutions to all problems, but they have more than proven themselves over time, so internalising the most common patterns can’t hurt. Design patterns are typically classified into three types:

Objects are made using creational patterns. Famous examples include the Factory Method and the Abstract Factory, as well as the controversial Singleton Pattern.

Structural patterns, such as the Adapter Pattern, the Decorator Pattern, and, of course, the Dependency Injection Pattern, combine objects into larger, but still flexible structures.

Behavioural patterns describe how objects interact with one another. The Iterator Pattern and Observer Pattern, for example, are commonly used in PHP.

The Dependency Injection pattern has already been discussed, but let’s take a closer look at the Observer Pattern. This pattern is useful when you want to react to an object’s actions without hardwiring this dependency into the code.

Assume we have a class that represents a customer account. When an account is terminated, we want one or more actions to be taken. To accomplish this, we can create observer (observer) classes that will be notified when this occurs. The actual customer account class, on the other hand, does not need to know which observers are involved in detail. Because this is a common pattern, we can already find objects in the previously mentioned SPL library to help us in implementing it. First, you can see the class for the customer account in Listing 6.

class CustomerAccount implements SplSubject
{
    private SplObjectStorage $observers;
 
    public function __construct()
    {
        $this->observers = new SplObjectStorage();
    }
 
    public function attach(SplObserver $observer): void
    {
        $this->observers->attach($observer);
    }
 
    public function detach(SplObserver $observer): void
    {
        $this->observers->detach($observer);
    }
 
    public function notify(): void
    {
        foreach ($this->observers as $observer) {
            $observer->update($this);
        }
    }
 
    public function cancelSubscription(): void
    {
        // ...
        $this->notify();
    }
}

Objects such as the ObjectStorage, in which the observers are managed, are already present. We have a working implementation of the Observer Pattern with only a few lines of code and no framework at all: when the cancelSubscription method is called, all Observers are notified.

Listing 7 shows an example of how such an observer is attached to a customer account.

class CustomerAccountObserver implements SplObserver
{
    public function update(CustomerAccount|SplSubject $splSubject): void
    {
        // ...
    }
}
 
$customerAccount = new CustomerAccount();

$customerAccount->attach(new CustomerAccountObserver());

$customerAccount->cancelSubscription();

As a result, the SPL library not only provides ready-made objects, but also interfaces like SplObserver, which ensure that the observer implements the necessary update method.

 

Code reviews

Now for the practical part: If you work in a team with other developers, one of the first tools you introduce should be code reviews. The four-eyes-principle in programming is implemented through code reviews: No code should be allowed to enter the codebase unchecked. Therefore, we ask other developers to review our code and approve it if it passes muster. Changes are requested if this is not the case.

This not only has the advantage of detecting bugs or other problems earlier, but it also leads to knowledge distribution within the team. As a reviewer, you not only gain an understanding of your colleagues’ code, but you also automatically exchange information about programming techniques, so everyone involved learns something.

Code reviews have become an essential part of many development teams’ workflows. Nevertheless, we would like to share some tips for successful code reviews with you in this article:

Make code reviews mandatory. They are a necessity rather than a nice-to-have. All major code hosting platforms provide the necessary features to prevent code from being merged into the Main Branch without being checked.

Nobody should be afraid of code reviews. The idea of experienced developers tearing code apart can be very intimidating, especially for colleagues with little professional experience. When reviewing, keep the authors’ skill level in mind at all times.

Take note of the wording, both yours and others’. Of course, accusations and insults are strictly prohibited, but feedback that lacks constructive suggestions will only lead to frustration. Because text comments lack important meta-information such as facial expression and voice pitch, a defusing emoji can go a long way. Provide examples and justifications for how it could be done better in the first person.

Before making assumptions, ask the authors why they programmed something the way they did. Maybe there’s a good reason you don’t know about?

Before you begin, agree on common coding styles and guidelines with the team.

Tools can be used to automate tasks such as reviewing coding styles and guidelines. Don’t waste time discussing how to format code. Tools are much faster and more thorough, and they are always objective.

The last two points, concerning coding styles, guidelines, and tools, will be discussed in greater depth in the following sections.

Coding styles

There are many ways to write and format code. Because we spend far more time reading than writing source code, it should be written as uniformly as possible. This reduces the cognitive load (mental effort) of reading and writing.

Instead of laboriously agreeing on a standard with your team, which usually results in entertaining discussions about “indenting with spaces or tabs,” you should instead rely on existing standards like PSR-12. PSR-12 is the current PHP-FIG (PHP Framework Interop Group) [3] coding style guide that defines basic code formatting. The benefit is that PSR-12 is widely used, and many tools, such as IDEs and code sniffers, already support it out of the box. Furthermore, many packages and frameworks use this standard, making their code easier to read when looking for errors, for example.

Coding guidelines

So, coding styles define how code should be formatted, but they don’t tell you anything about team-agreed-upon best practises or standards. This is where coding standards come in.

For example, you can specify that if you’re working with PHP 8 or later, you must use only the constructor property promotion (Listing 8).

//  Before PHP 8
class ExampleClass
{
    public string $name;
 
    public function __construct(
        string $name
    ) {
        $this->name = $name;
    }
}
 
// PHP 8 with constructor property promotion
class ExampleClass
{
    public function __construct(
        public string $name,
    ) {
    }
}

In addition to code-related guidelines, there are those relating to aspects of software architecture, such as compliance with the Single Responsibility Principle (SRP). However, specifications for folder structures and naming conventions should also be written down. Because most PHP applications are now built on a framework, it’s worth establishing some ground rules here, especially if the framework provides multiple ways to perform a task, such as configuring routing.

Unlike style guides, coding guidelines cannot be pre-made. Of course, there are numerous examples available on the Internet [4], but keep in mind that each codebase is uniqueSo you should adapt these templates in any case, and you probably won’t be able to adopt all points. It is best to create the guidelines as a team, even if requires several workshops. This ensures that the vast majority agrees.

 

Tools

The same is true when writing clean code, as is so often the case in programming: Automate as many steps as possible! After all, you don’t want to waste time in code review debating misplaced brackets. On the contrary, code should not be submitted for review if it does not meet certain minimum requirements.

This is exactly what we can achieve by employing code sniffers: PHP-CS-Fixer [5] and PHP CodeSniffer [6] are tools that check code for compliance with code style guides faster and more accurately than a human could. If they find an error, they provide an accurate bug report. Even better, the code sniffers can automatically correct the faulty code if desired. It doesn’t get any easier than this, and there are no more justifications for checking in incorrectly formatted code.

Which tool you use is a matter of personal preference. PSR-12 standards, for example, can handle both, and they can also be configured down to the smallest detail. But there’s a lot more. The PHP copy and paste detector phpcpd [7] can help you find duplicates. As a result, we effectively prevent single code parts from being copied arbitrarily back and forth.

The static code analysis is the final step. The code is thoroughly reviewed here for potential bugs or unclean programming. PHPStan [8] is probably the most well-known example of this genre in the PHP world. PHPStan analyses your code and indicates which parts may cause issues. You can begin with low requirements and gradually increase them.

There are, of course, other useful tools, such as Psalm [9], phpmd [10], Phan [11], and Exakat [12]. However, especially in the beginning, it is best to focus on one tool at a time to avoid becoming overwhelmed by the abundance of finds. You can then gradually add more tools.

Code metrics

Not only can code be analysed, but it can also be quantitatively evaluated. This is accomplished by employing code metrics such as Cyclomatic Complexity or NPath Complexity [13]. In short, the metrics indicate the complexity of the code under investigation. The more branches there are, the more difficult it is to understand and maintain. These metrics can help you determine the state of a codebase or provide specific hints about which classes to focus on first during a refactoring.

Without being able to go into these values in greater detail in this article, you should take away that there are established code metrics and that they can be collected automatically with the help of appropriate tools. PhpMetrics [14] and PHP Depend [15] are well-known guild representatives. However, keep in mind that if you do not understand the values, these tools will be useless. You will not be spared further study in this case.

Software tests

Software tests are not an afterthought in terms of clean code, but rather an essential component. But why are tests so important? Nobody writes perfect code the first time. We are always learning, and our own code from last year certainly does not meet our current requirements. This is completely normal and should not be suppressed. Rather, we should accept that constant refactoring, or reworking existing code while maintaining functionality, is a necessary part of the job. Everyone understands that machines require routine maintenance and cleaning. It is the same with code.

However, regular changes pose challenges: we must ensure that the actual functionality is not altered. Manual testing, such as clicking through the application monotonously, is error-prone and time-consuming. Automated tests can help in this situation.

However, software tests not only assist us in refactoring, but they also assist us in writing better code. If we test every new piece of code from the start, we are forced to write easily testable code — and testable code is especially important if, among other things, Dependency Inversion Principle (DIP) couplings are avoided.

A detailed introduction to automated tests could fill an entire book. Writing really good code without proper testing, on the other hand, is likely to be difficult, which is why you should get into the habit of testing every piece of code you write as soon as possible. Test Driven Development (TDD) does not have to be done exactly “by the book.”

Automation

If you’ve been wondering how the tools mentioned thus far can be most effectively integrated into your daily work routine, you’re absolutely correct. Because no matter how much work these tools relieve us of, as long as they must be executed manually, they are easily forgotten.

In an ideal world, no code should be added to the main branch without first passing a quality check. So, let us wrap up with the topic of continuous integration (CI). This is the repeated automatic execution of the same checking steps. Continuous quality assurance is an important aspect of clean code because without it, certain checks will always be overlooked or even omitted on purpose. We can basically distinguish two levels of checks here:

  1. Checks that are executed before checking into the repository (pre-commit)
  2. Checks that are executed during a pull request

Git hooks are commonly used to implement the first check level [16]. These are scripts that run during a git commit and abort the commit in case of a negative check. As test steps, we can run a code sniffer and a static code analyser, both of which we have already described in detail above.

This check may also include fast execution tests (typically unit tests). However, the execution time should be limited to a few dozen seconds to avoid overtaxing the programmer’s patience. At the second level, slower checks, such as end-to-end tests, are performed.

Once the code has passed the first set of checks, a pull request is typically issued, requesting that the code’s author merge the code into the main branch. The code reviews begin at this point. As previously stated, we want to ensure that the code being reviewed meets certain minimum requirements — all tests have passed without errors, the code is correctly formatted and error-free, at least in terms of static analysis. The code review should be performed only after these checks have been completed without errors.

Continuous integration is often described as a pipeline: the code mentally travels through a tube, stopping at various stations along the way. Different aspects of the code are tested at each station. If all checks are successful, the code is ready for delivery as a container image or, more traditionally, as a ZIP archive.

This pipeline can be divided into four sections roughly. Of course, you are free to use more or fewer steps; Figure 1 should only serve as a guide.

Fig. 1: Schematic breakdown of a build pipeline

Finally, the supreme discipline is continuous delivery. The code is not only thoroughly analyzed and tested, but (if there are no objections) it is also pushed out directly to the target system, e.g. a test environment or production, at the push of a button. Of course, this only works if you have a lot of confidence in the pipeline and it involves a lot of effort. But once you start working with it, you certainly won’t want to do without it.

IPC NEWSLETTER

All news about PHP and web development

 

Conclusion

The subject of clean code is extensive. Many different subareas intertwine here to produce clean code. We were only able to address the most important of them in this article. Regardless, you should have a good idea of how diverse and exciting the world of clean code is.

Don’t be concerned if you didn’t grasp everything right away. Writing clean code requires a significant amount of time, dedication, and, most importantly, practise — a lot of practise! However, the journey is worthwhile because you will quickly notice how the quality of your code improves. Is there a more powerful motivator?

Clean Code in PHP – The book

Have we piqued your curiosity about clean code? Packt Publishing recently published the book “Clean Code in PHP: Expert Tips and Best Practices to Write Beautiful, Human-Friendly, and Maintainable PHP” [12]. It delves into the topic of clean code in depth, with numerous PHP practical examples.


Links & References

[1] Martin, R.: „Clean Code. Refactoring, Patterns, Testen und Techniken für sauberen Code“ (1. Aufl.). mitp, 2009

[2] Martin, R.: „Clean Architecture. A Craftsman’s Guide to Software Structure and Design“ (1. Aufl.). Prentice Hall, 2018

[3] https://www.php-fig.org

[4] https://carstenwindler.de/software-quality/coding-guidelines/

[5] https://github.com/PHP-CS-Fixer/PHP-CS-Fixer

[6] https://github.com/squizlabs/PHP_CodeSniffer

[7] https://github.com/sebastianbergmann/phpcpd

[8] https://phpstan.org

[9] https://psalm.dev

[10] https://phpmd.org

[11] https://github.com/phan/phan

[12] https://www.exakat.io/en

[13] Windler, C. and Daubois, A.: “Clean Code in PHP. Expert tips and best practices to write beautiful, human-friendly, and maintainable PHP” (1. Aufl.). Packt Publishing, 2022

[14] https://phpmetrics.org

[15] https://pdepend.org

[16] https://githooks.com

Top Articles About Software Architecture

Stay tuned!

Register for our newsletter

Behind the Tracks of IPC

PHP Core
Best practices & applications

General Web Development
Broader web development topics

Test & Performance
Software testing and performance improvements

Agile & People
Getting agile right is so important

Software Architecture
All about PHP frameworks, concepts &
environments

DevOps & Deployment
Learn about DevOps and transform your development pipeline

Content Management Systems
Sessions on content management systems

#slideless (pure coding)
See how technology really works

Web Security
All about
web security

PUSH YOUR CODE FURTHER